/*
 * Copyright (c) 2011, Marc-Andre Ferland - madrang@gmail.com
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the The Warrent Team nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *     * This software is free for non-commercial use. You may not use this
 *       software, in whole or in part, in support of any commercial product
 *       without the express consent of the author.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE WARRENT TEAM BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
using System;
using System.Runtime.InteropServices;

namespace OsHelper.UsbNet.LibUsb.Transfer
{
	/// <summary>
	/// Asynchronous device I/O.
	/// </summary>
	/// <remarks>
	/// The user populates this structure and then submits it in order to request a transfer. 
	/// After the transfer has completed, the library populates the transfer with the results 
	/// and passes it back to the user.
	/// </remarks>
	[StructLayout(LayoutKind.Sequential)]
	public class Transfer : Handle, UsbNet.Transfer.iTransfer
	{
		[StructLayout(LayoutKind.Sequential)]
		private class LibUsbTransfer
		{
			public IntPtr DeviceHandle;
			public LibUsb.Transfer.Flags Flags;
			public byte Endpoint;
			public UsbNet.Descriptors.TransferType Type;
			public uint Timeout;
			public UsbNet.Transfer.Status Status;
			public int Length;
			public int ActualLength;
			public IntPtr CallbackHandle;
			public IntPtr UserDataHandle;
			public IntPtr BufferHandle;
			public int NumberIsoPackets;
			public IntPtr IsoPacketsHandle;
		}
		
		private static readonly int DeviceHandleOffset = Marshal.OffsetOf(typeof (LibUsbTransfer), "DeviceHandle").ToInt32();
		private static readonly int FlagsOffset = Marshal.OffsetOf(typeof (LibUsbTransfer), "Flags").ToInt32();
		private static readonly int EndpointOffset = Marshal.OffsetOf(typeof (LibUsbTransfer), "Endpoint").ToInt32();
		private static readonly int TypeOffset = Marshal.OffsetOf(typeof (LibUsbTransfer), "Type").ToInt32();
		private static readonly int TimeoutOffset = Marshal.OffsetOf(typeof (LibUsbTransfer), "Timeout").ToInt32();
		private static readonly int StatusOffset = Marshal.OffsetOf(typeof (LibUsbTransfer), "Status").ToInt32();
		private static readonly int LengthOffset = Marshal.OffsetOf(typeof (LibUsbTransfer), "Length").ToInt32();
		private static readonly int ActualLengthOffset = Marshal.OffsetOf(typeof (LibUsbTransfer), "ActualLength").ToInt32();
		private static readonly int CallbackHandleOffset = Marshal.OffsetOf(typeof (LibUsbTransfer), "CallbackHandle").ToInt32();
		private static readonly int UserDataHandleOffset = Marshal.OffsetOf(typeof (LibUsbTransfer), "UserDataHandle").ToInt32();
		private static readonly int BufferHandleOffset = Marshal.OffsetOf(typeof (LibUsbTransfer), "BufferHandle").ToInt32();
		private static readonly int NumberIsoPacketsOffset = Marshal.OffsetOf(typeof (LibUsbTransfer), "NumberIsoPackets").ToInt32();
		private static readonly int IsoPacketsHandleOffset = Marshal.OffsetOf(typeof (LibUsbTransfer), "IsoPacketsHandle").ToInt32();
		
		/// <summary>
		/// Asynchronous transfer callback delegate
		/// </summary>
		[UnmanagedFunctionPointer(0)]
		public delegate void LibUsbTransferDelegate(Transfer transfer);
		
		public event UsbNet.Transfer.TransferDelegate Completed;
		
		/// <summary>
		/// Allocate a libusb transfer with a specified number of isochronous packet descriptors 
		/// </summary>
		/// <remarks>
		/// <para>The transfer is pre-initialized for you.</para>
		/// <para>Transfers intended for non-isochronous endpoints (e.g. control, bulk, interrupt) should specify an iso_packets count of zero.</para>
		/// <para>For transfers intended for isochronous endpoints, specify an appropriate number of packet descriptors to be allocated as part of the transfer. The returned transfer is not specially initialized for isochronous I/O; you are still required to set the <see cref="Transfer.NumIsoPackets"/> and <see cref="MonoUsbTransfer.Type"/> fields accordingly.</para>
		/// <para>It is safe to allocate a transfer with some isochronous packets and then use it on a non-isochronous endpoint. If you do this, ensure that at time of submission, <see cref="Transfer.NumIsoPackets"/> is 0 and that type is set appropriately.</para>
		/// </remarks>
		/// <param name="IsoPacketsNumber">number of isochronous packet descriptors to allocate.</param>
		public Transfer (int IsoPacketsNumber) : base()
		{
			this.SetHandle (Transfer.AllocTransfer (IsoPacketsNumber));
			
			if (this.IsInvalid)
				throw new OutOfMemoryException ();
			
			LibUsbTransferDelegate del = new LibUsbTransferDelegate (this.CompletedEventCallback);
			this.CallbackHandle = Marshal.GetFunctionPointerForDelegate (del);
		}

		protected override bool ReleaseHandle ()
		{
			if (!this.IsInvalid) {
				Transfer.FreeTransfer (this.handle);
				this.SetHandleAsInvalid ();
			}
			return true;
		}
		
		private void CompletedEventCallback (Transfer transfer)
		{
			if (this.Completed != null)
				this.Completed (this);
		}

		/// <summary>
		/// Submits this transfer.
		/// </summary>
		/// <remarks>
		/// This functions submits the USB transfer and return immediately.
		/// </remarks>
		public void Submit ()
		{
			LibUsb.Error ErrorCode = Transfer.SubmitTransfer (this.DangerousGetHandle());
			
			switch (ErrorCode) {
			default:
				throw new Exception ("Unknown error in Transfer:Submit, LibUsb Code : " + ErrorCode);
			}
		}

		/// <summary>
		/// Cancels this transfer.
		/// </summary>
		public void Cancel ()
		{
			LibUsb.Error ErrorCode = Transfer.CancelTransfer (this.DangerousGetHandle());
			
			switch (ErrorCode) {
			default:
				throw new Exception ("Unknown error in Transfer:Cancel, LibUsb Code : " + ErrorCode);
			}
		}
		
		/// <summary>
		/// Gets the buffer data pointer.
		/// </summary>
        private IntPtr BufferHandle
        {
            get { return Marshal.ReadIntPtr(this.DangerousGetHandle(), Transfer.BufferHandleOffset); }
            set { Marshal.WriteIntPtr(this.DangerousGetHandle(), Transfer.BufferHandleOffset, value); }
        }

        /// <summary>
        /// User context data to pass to the callback function.
        /// </summary>
        private IntPtr UserDataHandle
        {
            get { return Marshal.ReadIntPtr(this.DangerousGetHandle(), Transfer.UserDataHandleOffset); }
            set { Marshal.WriteIntPtr(this.DangerousGetHandle(), Transfer.UserDataHandleOffset, value); }
        }

        /// <summary>
        /// Callback function pointer.
        /// </summary>
        /// <remarks>
        /// The callback function must be declared as a <see cref="LibUsbTransferDelegate"/>.
        /// </remarks>
        private IntPtr CallbackHandle
        {
            get { return Marshal.ReadIntPtr(this.DangerousGetHandle(), Transfer.CallbackHandleOffset); }
            set { Marshal.WriteIntPtr(this.DangerousGetHandle(), Transfer.CallbackHandleOffset, value); }
        }
		
		private IntPtr IsoPacketsHandle
        {
            get { return Marshal.ReadIntPtr(this.DangerousGetHandle(), Transfer.IsoPacketsHandleOffset); }
            set { Marshal.WriteIntPtr(this.DangerousGetHandle(), Transfer.IsoPacketsHandleOffset, value); }
        }

        /// <summary>
        /// Actual length of data that was transferred. 
        /// </summary>
        public int ActualLength
        {
			get { return Marshal.ReadInt32(this.DangerousGetHandle(), Transfer.ActualLengthOffset); }
			private set { Marshal.WriteInt32(this.DangerousGetHandle(), Transfer.ActualLengthOffset, value); }
        }

        /// <summary>
        /// Length of the data buffer.
        /// </summary>
        public int Length
        {
            get { return Marshal.ReadInt32(this.DangerousGetHandle(), Transfer.LengthOffset); }
			private set { Marshal.WriteInt32(this.DangerousGetHandle(), Transfer.LengthOffset, value); }
        }

        /// <summary>
        /// The status of the transfer.
        /// </summary>
		public UsbNet.Transfer.Status Status
		{
			get { return (UsbNet.Transfer.Status)Marshal.ReadInt32(this.DangerousGetHandle(), Transfer.StatusOffset); }
			private set { Marshal.WriteInt32(this.DangerousGetHandle(), Transfer.StatusOffset, (int)value); }
		}

        /// <summary>
        /// Timeout for this transfer in millseconds.
        /// </summary>
        public int Timeout
        {
            get { return Marshal.ReadInt32(this.DangerousGetHandle(), Transfer.TimeoutOffset); }
            set { Marshal.WriteInt32(this.DangerousGetHandle(), Transfer.TimeoutOffset, value); }
        }

        /// <summary>
        /// Type of the endpoint. 
        /// </summary>
        private UsbNet.Descriptors.TransferType Type
        {
            get { return (UsbNet.Descriptors.TransferType)Marshal.ReadByte(this.DangerousGetHandle(), Transfer.TypeOffset); }
            set { Marshal.WriteByte(this.DangerousGetHandle(), Transfer.TypeOffset, (byte)value); }
        }

        /// <summary>
        /// Enpoint address.
        /// </summary>
        private byte Endpoint
        {
            get { return Marshal.ReadByte(this.DangerousGetHandle(), Transfer.EndpointOffset); }
            set { Marshal.WriteByte(this.DangerousGetHandle(), Transfer.EndpointOffset, value); }
        }

        /// <summary>
        /// A bitwise OR combination of <see cref="Flags"/>.
        /// </summary>
        public Flags Flags
        {
            get { return (Flags)Marshal.ReadByte(this.DangerousGetHandle(), Transfer.FlagsOffset); }
            set { Marshal.WriteByte(this.DangerousGetHandle(), Transfer.FlagsOffset, (byte)value); }
        }

        /// <summary>
        /// Raw device handle pointer.
        /// </summary>
		private IntPtr DeviceHandle
        {
            get { return Marshal.ReadIntPtr(this.DangerousGetHandle(), Transfer.DeviceHandleOffset); }
            set { Marshal.WriteIntPtr(this.DangerousGetHandle(), Transfer.DeviceHandleOffset, value); }
        }

        /// <summary>
        /// Number of isochronous packets. 
        /// </summary>
        public int NumberIsoPackets
        {
            get { return Marshal.ReadInt32(this.DangerousGetHandle(), Transfer.NumberIsoPacketsOffset); }
            set { Marshal.WriteInt32(this.DangerousGetHandle(), Transfer.NumberIsoPacketsOffset, value); }
        }

		/// <summary>
		/// Device to send data to.
		/// </summary>
		public LibUsb.Device Device {
			get {
				Device Result = null;
				Device.GetDevice(this.DeviceHandle, out Result);
				return Result;
			}
			set { this.DeviceHandle = value.DangerousGetHandle(); }
		}
		
		/// <summary>
		/// Gets a <see cref="IsoPacket"/> that represents the specified iso packet descriptor. 
		/// </summary>
		/// <param name="PacketNumber">The iso packet descriptor to return.</param>
		/// <returns>The <see cref="IsoPacket"/> that represents <paramref name="PacketNumber"/>.</returns>
		public UsbNet.Transfer.IsoPacket GetIsoPacket (int PacketNumber)
		{
			return GetIsoPacketInternal(PacketNumber);
		}
		
		private IsoPacket GetIsoPacketInternal (int PacketNumber)
		{
			if (PacketNumber > this.NumberIsoPackets)
				throw new ArgumentOutOfRangeException ("PacketNumber");
			
			IntPtr IsoPacketHandle = new IntPtr (this.IsoPacketsHandle.ToInt64 () + (PacketNumber * Marshal.SizeOf (typeof(LibUsbTransfer))));
			
			return new IsoPacket (IsoPacketHandle);
		}

		/// <summary>
		/// Convenience function to locate the position of an isochronous packet within
		/// the buffer of an isochronous transfer. 
		/// </summary>
		/// <remarks>
		/// <para>This is a thorough function which loops through all preceding packets,
		/// accumulating their lengths to find the position of the specified packet.
		/// Typically you will assign equal lengths to each packet in the transfer,
		/// and hence the above method is sub-optimal.
		/// You may wish to use <see cref="GetIsoPacketBufferSimple"/> instead.</para>
		/// <note title="Libusb-1.0 API Note:" type="cpp">
		/// <see cref="GetIsoPacketBuffer"/> is roughly equivalent to
		/// <a href="http://libusb.sourceforge.net/api-1.0/group__asyncio.html#ga7f6ea0eb35a216d19d984977e454a7b3">libusb_get_iso_packet_buffer()</a>.
		/// </note>
		/// </remarks>
		/// <param name="packet">The packet to return the address of.</param>
		/// <returns>the base address of the packet buffer inside the transfer buffer.</returns>
		/// <exception cref="ArgumentOutOfRangeException">This exception is thrown if the packet requested is >= <see cref="NumIsoPackets"/>.</exception>
		private IntPtr GetIsoPacketBufferInternal (int packet)
		{
			if (packet >= this.NumberIsoPackets)
				throw new ArgumentOutOfRangeException ("packet", "GetIsoPacketBuffer: packet must be < NumIsoPackets");
			
			long offset = this.BufferHandle.ToInt64 ();
			
			for (int i = 0; i < packet; i++)
				offset += this.GetIsoPacket(i).Length;
			
			return new IntPtr (offset);
		}

		/// <summary>
		/// Convenience function to locate the position of an isochronous packet
		/// within the buffer of an isochronous transfer,
		/// for transfers where each packet is of identical size.
		/// </summary>
		/// <remarks>
		/// <para>This function relies on the assumption that every packet within the transfer is of identical size to the first packet. Calculating the location of the packet buffer is then just a simple calculation: buffer + (packet_size * packet)</para>
		/// <para>Do not use this function on transfers other than those that have identical packet lengths for each packet.</para>
		/// <note title="Libusb-1.0 API Note:" type="cpp">
		/// <see cref="GetIsoPacketBufferSimple"/> is roughly equivalent to
		/// <a href="http://libusb.sourceforge.net/api-1.0/group__asyncio.html#ga3df9a28c4f5c8f1850181ddb5efd12fd">libusb_get_iso_packet_buffer_simple()</a>.
		/// </note>        
		/// </remarks>
		/// <param name="packet">The packet to return the address of.</param>
		/// <returns>the base address of the packet buffer inside the transfer buffer.</returns>
		/// <exception cref="ArgumentOutOfRangeException">This exception is thrown if the packet requested is >= <see cref="NumIsoPackets"/>.</exception>
		private IntPtr GetIsoPacketBufferSimpleInternal (int packet)
		{
			if (packet >= this.NumberIsoPackets)
				throw new ArgumentOutOfRangeException ("packet", "GetIsoPacketBufferSimple: packet must be < NumIsoPackets");
			
			return new IntPtr ((this.BufferHandle.ToInt64 () + (this.GetIsoPacket(0).Length * packet)));
		}

		/// <summary>
		/// Convenience function to set the length of all packets in an isochronous transfer, based on the num_iso_packets field in the transfer structure. 
		/// </summary>
		/// <remarks>
		/// <note title="Libusb-1.0 API Note:" type="cpp">
		/// <see cref="SetIsoPacketLengths"/> is roughly equivalent to
		/// <a href="http://libusb.sourceforge.net/api-1.0/group__asyncio.html#gacbdecd6f50093f0c1d0e72ee35ace274">libusb_set_iso_packet_lengths()</a>.
		/// </note>   
		/// </remarks>
		/// <param name="length">The length to set in each isochronous packet descriptor.</param>
		public void SetIsoPacketLengths (int length)
		{
			for (int i = 0; i < this.NumberIsoPackets; i++)
				this.GetIsoPacketInternal(i).Length = length;
		}

		[DllImport("libusb-1.0.dll", SetLastError = false, EntryPoint = "libusb_alloc_transfer")]
		static internal extern IntPtr AllocTransfer (int isoPackets);

		[DllImport("libusb-1.0.dll", SetLastError = false, EntryPoint = "libusb_free_transfer")]
		static internal extern void FreeTransfer (IntPtr pTransfer);

		[DllImport("libusb-1.0.dll", SetLastError = false, EntryPoint = "libusb_submit_transfer")]
		static internal extern LibUsb.Error SubmitTransfer (IntPtr pTransfer);

		[DllImport("libusb-1.0.dll", SetLastError = false, EntryPoint = "libusb_cancel_transfer")]
		static internal extern LibUsb.Error CancelTransfer (IntPtr pTransfer);
	}
}
